#include "Drawer.h"
#include "World.h"
#include "ImageBitmap.h"
#include "Polygon.h"
#include "PolygonData.h"
#include "Coordinates.h"


#include <algorithm>
using std::swap;
using std::vector;

namespace HW2
{
	Drawer::Drawer(void)
	{
		reset();
		objectSpace = false;
		projectionOrthographic = Matrix::orthographic();
	}

	void Drawer::reset()
	{
		color = Color(0,0,0);
		transformation = Matrix::I();
		scaleSize = 1;
	}

	void Drawer::resize(int cx, int cy) // Resize the window, scale appropriately
	{
		double AspectRatio = (double)cy/(double)cx;
		
		setCenter(cx/2, cy /2);
		double initialRatio = HW2::World::get().initialRatio;
		double scaleToApply;
		scaleToApply = min(cx / initialWindowSize.X(), scaleToApply = cy/ initialWindowSize.Y());
		scale(scaleToApply);
	}


	void Drawer::setScreen(CDC* screen)
	{
		this->screen = screen;

	}

	void Drawer::setColor(const  Color &c)
	{
		color = c;
	}

	void Drawer::drawEdges(std::vector<Edge> &v)
	{
		for(unsigned i = 0; i < v.size(); ++i)
		{
			const HW2::Edge &e = v[i];
			setColor(e.color);
			drawLineOnPlotter(transformAndProject(e.start),transformAndProject(e.end), ScreenPlotter(screen));
		}
	}

	void Drawer::apply(Matrix &m)
	{
		transformation.multiplySide(m, objectSpace);
	}

	void Drawer::translate(double x, double y, double z)
	{
		apply(Matrix::move(x,y,z));
	}


	void Drawer::plot(int x, int y)
	{
		screen->SetPixel(x,y,RGB(color.r,color.g,color.b));
	}

	double Drawer::getNiceRatio(Coordinates low, Coordinates high, int windowWidth, int windowHeight)
	{
		Coordinates diff = high - low;

		double width = abs(diff.X());
		double height = abs(diff.Y());
		double ratioX = windowWidth / width;
		double ratioY = windowHeight / height;
		double ratio = min(ratioX, ratioY);

		ratio *= 0.9; // adjustement, so that it doesn't get to the borders
		return ratio;
	}

	void Drawer::OnFinishFileLoad(int windowWidth, int windowHeight)
	{
		reset();

		World &w = World::get();

		/////////////////////////////////////////
		///////////// Scaling ///////////////////
		/////////////////////////////////////////
		Coordinates low = w.minPoint, high = w.maxPoint;
		double ratio = getNiceRatio(low, high, windowWidth, windowHeight);
		transformation = Matrix::scale(ratio);

		/////////////////////////////////////////
		///////////// Rotating ///////////////////
		/////////////////////////////////////////
		// The object in IRIT files is always inverted, so we reverse it
		transformation *= Matrix::rotateX(3.14159265);

		/////////////////////////////////////////
		///////////// Projection/////////////////
		/////////////////////////////////////////
		low *= ratio;
		high *= ratio;
		Coordinates diff = high - low;
		double depth = diff.Z();
		projectionPerspective = Matrix::perspective(diff.norm()); 
		offsetZBeforeProjection = diff.Z() + diff.norm();
		//projection = Matrix::orthographic(); 


		/////////////////////////////////////////
		/////////// Offset (to center) //////////
		/////////////////////////////////////////
		// We know the object is always centered on (0,0,0)
		centerX = windowWidth /2;
		centerY = windowHeight /2;

		/////////////////////////////////////////
		/////////// Initial Window size /////////
		/////////////////////////////////////////
		if(windowHeight / (double)windowWidth <= w.initialRatio)
			initialWindowSize = Coordinates(windowHeight/w.initialRatio, windowHeight);
		else
			initialWindowSize = Coordinates(windowWidth, windowWidth * w.initialRatio);
	}





	Coordinates Drawer::transform(Coordinates p)
	{
		return p *= transformation;
	}

	Coordinates Drawer::project(Coordinates p)
	{
		if(World::get().isPerspective)
		{
			p.Z() += offsetZBeforeProjection;
			double z = p.Z();
			p *= projectionPerspective;
			p.Z() = z;
		}
		p.X() += centerX;
		p.Y() += centerY;
		return p;
	}

	Coordinates Drawer::transformAndProject(Coordinates p)
	{
		return project(transform(p));
	}

	void Drawer::scale(double ratio)
	{
		transformation *= Matrix::scale(ratio / scaleSize);
		scaleSize = ratio;
	}

	void Drawer::rotateX(double a)
	{
		apply(Matrix::rotateX(a));
	}

	void Drawer::rotateY(double a)
	{
		apply(Matrix::rotateY(a));
	}

	void Drawer::rotateZ(double a)
	{
		apply(Matrix::rotateZ(a));
	}

	void Drawer::modifyD(double scale)
	{
		projectionPerspective(3,4) /= scale;
	}


	template<typename PLOTTER>
	void Drawer::drawLineOnPlotter(Coordinates c1, Coordinates c2, PLOTTER &p, Coordinates beforeTransfoStart, Coordinates beforeTransfoEnd, Coordinates OriginalStart, Coordinates OriginalEnd )
	{
		int x1 = c1.X(), y1 = c1.Y(), x2 = c2.X(), y2 = c2.Y();
		double z1 = c1.Z(), z2 = c2.Z();

		bool steep = abs(y2 - y1) > abs(x2 - x1);
		if (steep){
			swap(x1, y1);
			swap(x2, y2);
		}
		
		if (x1 > x2){
			swap(x1, x2);
			swap(y1, y2);
			swap(z1, z2);
			swap(beforeTransfoStart, beforeTransfoEnd);
			swap(OriginalStart, OriginalEnd);
		}
		
		int dx = x2 - x1, dy = abs(y2 - y1);
		int error = dx / 2;
		int ystep, y = y1;
		if (y1 < y2) 
			ystep = 1;
		else
			ystep = -1;
		double zstep = (dx == 0? 0: (z2-z1) / dx );
		double z = z1;
		for (int x= x1; x <= x2; x++, z+=zstep)
		{
			Coordinates beforeTransformation = Coordinates::Interpolate(beforeTransfoStart, beforeTransfoEnd, ((double)(x-x1)) / dx);
			Coordinates original = Coordinates::Interpolate(OriginalStart, OriginalEnd, ((double)(x-x1)) / dx);
			if (steep)
				p.plot(y,x,z, color, beforeTransformation, original);
			else 
				p.plot(x,y,z, color, beforeTransformation, original);
			error -= dy;
			if (error < 0)
			{
				y = y + ystep;
				error += dx;
			}
		}
	}
	
	
	void Drawer::drawLineOnPlotter(Coordinates c1, Coordinates c2, Plotter &p)
	{
		int x1 = c1.X(), y1 = c1.Y(), x2 = c2.X(), y2 = c2.Y();
		double z1 = c1.Z(), z2 = c2.Z();

		bool steep = abs(y2 - y1) > abs(x2 - x1);
		if (steep){
			swap(x1, y1);
			swap(x2, y2);
		}
		if (x1 > x2){
			swap(x1, x2);
			swap(y1, y2);
			swap(z1, z2);
		}
		int dx = x2 - x1, dy = abs(y2 - y1);
		int error = dx / 2;
		int ystep, y = y1;
		if (y1 < y2) 
			ystep = 1;
		else
			ystep = -1;
		double zstep = (dx == 0? 0: (z2-z1) / dx );
		double z = z1;
		for (int x= x1; x <= x2; x++, z+=zstep)
		{
			
			if (steep)
				p.plot(y,x,z,color);
			else 
				p.plot(x,y,z,color);
			error -= dy;
			if (error < 0)
			{
				y = y + ystep;
				error += dx;
			}
		}
	}

	PolygonData Drawer::generatePolygonData(Polygon &p)
	{
		PolygonData pp(p);
		World &w = World::get();
		unsigned int size = p.coordinates.size();
		// v1 -> transformed
		// v2 -> projected
		std::vector<Coordinates> v1(size), v2(size);
		for(unsigned int i = 0; i < size; ++i)
		{
			v1[i] = transform(p.coordinates[i]);
			v2[i] = project(v1[i]);
		}
		for(unsigned int i = 0; i < size -1 ; ++i)
			drawLineOnPlotter(v2[i], v2[i+1], pp, v1[i], v1[i+1], p.coordinates[i], p.coordinates[i+1] );
		drawLineOnPlotter(v2[size-1], v2[0], pp, v1[size-1], v1[0], p.coordinates[size-1], p.coordinates[0]);
		return pp;
	}
	//*/
	ImageBitmap Drawer::generateBitmap(Polygon &p)
	{
		World &w = World::get();
		double minZ = 1<<30, maxZ = -(1<<30);
		ImageBitmap I;
		I.p = &p;

		std::vector<Point2D> &v = I.points; 
		for(unsigned  int i = 0; i < p.coordinates.size(); ++i)
		{ 
			Coordinates coor = transformAndProject(p.coordinates[i]);
			Point2D pp = coor.toPoint2D(); 
			v.push_back(pp);
			minZ = min(coor.Z(), minZ);
			maxZ = max(coor.Z(), maxZ);
			I.minPoint.x = min(I.minPoint.x, pp.x);
			I.minPoint.y = min(I.minPoint.y, pp.y);

			I.maxPoint.x = max(I.maxPoint.x, pp.x);
			I.maxPoint.y = max(I.maxPoint.y, pp.y);
		}
		I.z = (minZ + maxZ);
		for(unsigned int i = 0; i < v.size() -1 ; ++i)
			drawLineOnPlotter(v[i].toCoordinates(), v[i+1].toCoordinates(), I);
		drawLineOnPlotter(v[v.size()-1].toCoordinates(), v[0].toCoordinates(), I);
		return I;
	}
}